# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import logging

from coin.base.timestamp import get_timestamp
from coin.exchange.base.tick import FixedTickPrice
from coin.exchange.base.order_gateway import OrderSide, OrderType


class BitmexStopLossExecutor(object):
  def __init__(self, execution_params, order_gateway, logger=None):
    self._logger = logger or logging.getLogger('BitmexStopLossExecutor')
    self._og = order_gateway

    self._product = execution_params['product']
    self._native_symbol = execution_params['product'].native_symbol
    self._qty_add = execution_params.get('qty_add', 0.)  # Usually lot_size.
    self._trigger_threshold = execution_params['trigger_threshold']
    self._execution_payup = execution_params['execution_payup']
    self._order_update_period = (execution_params.get('order_update_period', 2) * (10**9))

    self._tick = FixedTickPrice(0.5)  # TODO(jaewon): Parameterize

    self._pos = 0
    self._sell_on_the_fly_orders = []
    self._sell_untriggered_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_untriggered_orders = []
    self._triggered_orders = []

    self._last_fire_time = 0
    self._create_list = []
    self._amend_list = []

  def _update_orders(self):
    self._pos = self._og.get_position(self._product)

    self._sell_on_the_fly_orders = []
    self._sell_untriggered_orders = []
    self._buy_on_the_fly_orders = []
    self._buy_untriggered_orders = []
    self._triggered_orders = []

    for order in self._og.get_working_order():
      if (order.product != self._native_symbol or not order.order_id.startswith('stoploss-')
          or order.internal.trigger_price is None):
        continue

      if order.order_side == OrderSide.SELL:
        if not order.accepted:
          self._sell_on_the_fly_orders.append(order)
        elif not order.internal.triggered:
          self._sell_untriggered_orders.append(order)
        else:
          self._triggered_orders.append(order)

      elif order.order_side == OrderSide.BUY:
        if not order.accepted:
          self._buy_on_the_fly_orders.append(order)
        elif not order.internal.triggered:
          self._buy_untriggered_orders.append(order)
        else:
          self._triggered_orders.append(order)

      else:
        self._logger.error('Unknown side: %s', str(order))
        continue

  def _cancel_all_triggered_orders(self):
    for order in self._triggered_orders:
      self._amend_list.append(self._og.make_bitmex_amend(order.order_id, leaves_qty=0))

  def _cancel_sell_untriggered_orders(self, exclude_order_id=None):
    for order in self._sell_untriggered_orders:
      if order.order_id == exclude_order_id:
        continue
      self._amend_list.append(self._og.make_bitmex_amend(order.order_id, leaves_qty=0))

  def _cancel_buy_untriggered_orders(self, exclude_order_id=None):
    for order in self._buy_untriggered_orders:
      if order.order_id == exclude_order_id:
        continue
      self._amend_list.append(self._og.make_bitmex_amend(order.order_id, leaves_qty=0))

  def _manage_sell_order(self, sell_price, sell_trigger_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (sell_price is None or self._sell_on_the_fly_orders or self._pos <= -self._qty_add):
      self._cancel_sell_untriggered_orders()
      return

    sell_qty = self._pos + self._qty_add
    assert sell_qty > 0, self._pos

    # Create new order if there is no working order.
    if not self._sell_untriggered_orders:
      order_id = 'stoploss-%d' % get_timestamp()
      order = self._og.make_bitmex_order(symbol=self._native_symbol,
                                         price=sell_price,
                                         qty=sell_qty,
                                         order_side=OrderSide.SELL,
                                         order_type=OrderType.LIMIT,
                                         order_id=order_id,
                                         trigger_price=sell_trigger_price,
                                         tag='stoploss')
      self._create_list.append(order)
      self._logger.info('SELL-STOPLIMIT %s %f @ %.1f, trigger=%.1f, tag=stoploss',
                        self._native_symbol,
                        sell_qty,
                        sell_price,
                        sell_trigger_price)
      return

    # If there is an working order that its price is same to sell_trigger_price,
    # keep it and cancel all other orders.
    for order in self._sell_untriggered_orders:
      if (order.internal.trigger_price == sell_trigger_price and order.qty == sell_qty):
        self._cancel_sell_untriggered_orders(exclude_order_id=order.order_id)
        return

    # Choose one with the smallest trigger price and modify it and
    # cancel all other orders.
    for idx, order in enumerate(self._sell_untriggered_orders):
      if idx == 0:
        chosen_order = order
        continue
      if order.internal.trigger_price < chosen_order.internal.trigger_price:
        chosen_order = order

    self._cancel_sell_untriggered_orders(exclude_order_id=chosen_order.order_id)
    self._amend_list.append(
        self._og.make_bitmex_amend(order_id=chosen_order.order_id,
                                   price=sell_price,
                                   leaves_qty=sell_qty,
                                   trigger_price=sell_trigger_price))
    self._logger.info('SELL-STOPLIMIT %s %f @ %.1f, trigger=%.1f, tag=stoploss',
                      self._native_symbol,
                      sell_qty,
                      sell_price,
                      sell_trigger_price)

  def _manage_buy_order(self, buy_price, buy_trigger_price):
    # If there is an on-the-fly order, cancel all working orders and keep the
    # on-the-fly order.
    if (buy_price is None or self._buy_on_the_fly_orders or self._pos >= self._qty_add):
      self._cancel_buy_untriggered_orders()
      return

    buy_qty = -self._pos + self._qty_add
    assert buy_qty > 0, self._pos

    # Create new order if there is no working order.
    if not self._buy_untriggered_orders:
      order_id = 'stoploss-%d' % get_timestamp()
      order = self._og.make_bitmex_order(symbol=self._native_symbol,
                                         price=buy_price,
                                         qty=buy_qty,
                                         order_side=OrderSide.BUY,
                                         order_type=OrderType.LIMIT,
                                         order_id=order_id,
                                         trigger_price=buy_trigger_price,
                                         tag='stoploss')
      self._create_list.append(order)
      self._logger.info('BUY-STOPLIMIT  %s %f @ %.1f, trigger=%.1f, tag=stoploss',
                        self._native_symbol,
                        buy_qty,
                        buy_price,
                        buy_trigger_price)
      return

    # If there is an working order that its price is same to buy_trigger_price,
    # keep it and cancel all other orders.
    for order in self._buy_untriggered_orders:
      if (order.internal.trigger_price == buy_trigger_price and order.qty == buy_qty):
        self._cancel_buy_untriggered_orders(exclude_order_id=order.order_id)
        return

    # Choose one with the biggest trigger price and modify it and
    # cancel all other orders.
    for idx, order in enumerate(self._buy_untriggered_orders):
      if idx == 0:
        chosen_order = order
        continue
      if order.internal.trigger_price > chosen_order.internal.trigger_price:
        chosen_order = order

    self._cancel_buy_untriggered_orders(exclude_order_id=chosen_order.order_id)
    self._amend_list.append(
        self._og.make_bitmex_amend(order_id=chosen_order.order_id,
                                   price=buy_price,
                                   leaves_qty=buy_qty,
                                   trigger_price=buy_trigger_price))
    self._logger.info('BUY-STOPLIMIT  %s %f @ %.1f, trigger=%.1f, tag=stoploss',
                      self._native_symbol,
                      buy_qty,
                      buy_price,
                      buy_trigger_price)

  def execute(self, create_list, amend_list):
    if get_timestamp() - self._last_fire_time < self._order_update_period:
      return

    if amend_list:
      self._og.amend_bulk(amend_list)
      self._logger.debug('Amend %d orders', len(amend_list))

    if create_list:
      self._og.submit_bulk(create_list)
      self._logger.debug('Create %d orders', len(create_list))

    if create_list or amend_list:
      self._last_fire_time = get_timestamp()

  def manage_orders(self,
                    ref_ask_price,
                    ref_bid_price,
                    create_list=None,
                    amend_list=None,
                    fire=True):
    self._create_list = [] if create_list is None else create_list
    self._amend_list = [] if amend_list is None else amend_list
    self._update_orders()

    sell_price = self._tick.floor(ref_bid_price * (1. - self._execution_payup))
    sell_trigger_price = self._tick.floor(ref_bid_price * (1. - self._trigger_threshold))
    buy_price = self._tick.ceil(ref_ask_price * (1. + self._execution_payup))
    buy_trigger_price = self._tick.ceil(ref_ask_price * (1. + self._trigger_threshold))

    self._cancel_all_triggered_orders()
    self._manage_sell_order(sell_price, sell_trigger_price)
    self._manage_buy_order(buy_price, buy_trigger_price)

    if fire:
      self.execute(self._create_list, self._amend_list)
